ScalaでActorを使ったメッセージパッシング便利化ライブラリを書いてみた
(旧)ScalaBaseに行けないエントリ (駄目な事書きすぎで事務所的にアウトが出ました)
概要
メッセージパッシング便利化ライブラリ作った
ScalaMessengerPrototype
https://github.com/sassembla/ScalaMessengerPrototype
Scalaで標準に用意されているActor(2.9.2までなのでActors)
http://www.scala-lang.org/api/current/scala/actors/Actor.html
を、特にclass間のmessagePassingに使用するために使いやすくしたつもりのライブラリ。
ちょうど2.10がそろそろ出るぜ的な話があったので、
Akkaに取って替わられるActorsにお別れを言いがてら作ってみた。
コンセプト/要件定義
MessagePasingが、特定のフォーマット+小さな手続きで使用/送受信可能
要件
・Javaから使える/Scalaから使える
・通信内部は全部Actors、Akka版へのリプレースを考慮に入れる
・同期、非同期でのメッセージ送付/受信
・ネスト呼び出し可
・ブロードキャストあり
・通信が可能な対象を明示する機構
・メッセージラベルの付与
・可変長引数でのオブジェクトの送付
・テスト書く
・動作にかかるコスト、処理のチューニングは無視
オリジナルは、Objective-CのNSNotificationを使ったもの。
このへん。
https://github.com/sassembla/MessengerSystem
注:古いです。ARC対応してません。ぶっちゃけARC対応した最新の社内版があるのだけど、特化しすぎてて出せない。
コレを使うと何がどう楽になるの?
メッセージパッシングのそもそもの利点として、
pointer/参照を持たないオブジェクト同士が、物を投げ合ったりする事が出来る
っつーのがあるのですが、
ただ、Actorsそのままだと、毎回定義するものが多かったり、case class作りまくらないといけなかったりで、めんどい。
なので、
数行書けば使える
みたいな目標をたてて、一通りMessagePassingが、
特定のフォーマット+小さな手続きで使用/送受信可能になるように、作ってました。
実際、Javaからだと6行くらい、Scalaからだと4行くらい書けば使えます。
列挙すると
①特定フォーマットでメッセージを送れる
def call(targetName : String, exec : String, message : Array[TagValue])
targetName
メッセージ送付ターゲットの識別子、メールでいうところのTo
exec
メッセージの識別子、メールでいうところのタイトル
message
可変長引数のkey-valueペア。本文みたいなもの。
②特定の受信箇所に届く(必ず書かなきゃいけない)
/**
* Childのレシーバ
*/
@Override
public void receiver(String exec, TagValue[] tagValues) {
.......
無いとコンパイルとおらない。
サンプルコード
Javaから使う
親
子
Scalaから使う
親
子
制約
PROBLEM:
pointerに依存せずに自由に物を送り合う事が出来るので、自由にやり過ぎるとカオスになる
受信者の名前だけを指定して送る形式だった時代があって、誰が誰に何を送ってるかがカオスになった。
SOLUTION;
明示した関係の間柄でしか、送付しあえないようにする
具体的には→
[親子関係がある間柄しかmessageを送付しあえない]という制約を勝手に作っている。
下記シークエンスで成立させてる。
・messengerが各自なまえを持つ
・子どもになる予定のmessengerからinputParent(親名称)で仮親を名前で指定
・親予定のmessengerが受信、条件を満たしていたら子予定を子どもとして認定、子どもに返信
・親子関係成立
→制約が有る上でのデメリット
・誰にでも送る、という動作ができなくなった
・本来余剰な機構なので重い
→制約がある上でのメリット
・誰に送るつもりか、コードの特定の箇所をみればわかる
デメリットの方が多いんだけど、複数人で使う上では、メッセージの方向の明示性は必須仕様だった。
MessagePassingで書くと楽なこと
・オブジェクト間の関係性をPointerとは無関係に再構築可能
Pointerでの構造的な構築とは別に、任意の意味的な関係の構築が可能。
既存の手段 : PointerでのObject所持、メソッド実行
に、
新手段 : MessagePassingでの関係構築
が加わる感じ。
道具が増える分だけ、明示できるものが増える。
・粗結合
MessagePassing自体の前提なので、コレを使って使われているライブラリとかモジュールも、粗結合にできる。
さらに言うとしやすい。
・意味の付加が可能
発生させるメッセージを「イベント」だとすると、下記みたいな表記の発信と受け取りが可能。
例 リロードボタンが押されて、表示データが更新される
(ビュー:タッチが発生
→この部分はだいたいの仕様で暗黙のはず)
→ビューコントローラ:タッチイベントが発生
→RELOAD_TOUCHED をだれかに発信
→データ保存してるコントローラ:RELOAD_TOUCHEDを受け取る
→保存されてるデータを再取得(更新)
→更新されたデータを発信 DATA_RELOADED
→ビュー:DATA_RELOADED を受け取ってデータ表示更新
とりあえず書いてモヤッとしてるところ
Scalaからだと、使うオブジェクトがextends必須
→Javaから書いたので残ってしまった超残念な部分。Akkanizeさせたときに消す。
コアがシングルトンなobject
→一番なんとかならんかなーって思った箇所で、Actorsのchannelとかをちゃんと使えば出来たような気がします。
次必ず消します。
ScalaScalaしてない
→ログの機構とかにListBuffer使いまくってます。吊ってきます。
キャストって言う動作が完全に終わってる
→Akka版作るときは型変換効かせたいです。
Javaから使うのを優先しすぎた感ある
→Javaでテストを書いていたので、書きにくい形式を選べませんでした吊ってきます。
次回からScalaオンリーの予定。
遅い
→Javaから使って、messageが同期で届くまでに0.01秒くらいかかってる感じ。
ボトルネックは、通信のたびに小さな内部Actorを作っている事。
Actorsの実装だと、Actorから自分自身を呼び出すとロックする。
それを回避するために、自分自身へのメッセージ送付を肩代わりするActorが一瞬だけ生成される。
例えば下記の、親を呼ぶメソッドからは、
def callParent(exec : String, message : Array[TagValue]) = {
log += Log.LOG_TYPE_CALLPARENT.toString
val future = parent(0).duplicate !! CallParent(exec, message)
val result = future()
result match {
case Done(_) =>
case Failure(reason) =>
}
}
duplicateメソッドでActorを一つ作って、ネストした際のロックを避けるようになっている。
def duplicate : SubMessengerActor = {
new SubMessengerActor(this, myself)
}
で、SubMessengerActorの実装は、まんまActor。
class SubMessengerActor(master : MessengerActor, myself : MessengerProtocol) extends Actor {
start
def act() = {
loop {
...
Actorの中に、通信のたびに別のActorが生まれて、終わったら消える。
こんなのがメッセージごとにあるので、めっちゃコスト喰うし、遅い。
Akkaだとネストしても平気なので、デフォで似たような機構が入ってるのかもしれないけど、最適化とかいろいろホラ。。
GCあるのに、Dealloc相当のメソッドがある
→Actorの都合上、停止は明示的に書かないといけないです。
このライブラリのMessengerも、
closeでMessengerだけを、
closeSystemで「系」全体を停止させます。
どうすればいいのかわかりません。GCさんよろしくお願いします、、よろ、、し、、、
今後
Akka化にあたって、サーバ間でのremoteをサポートして、remote間でのMessagingを簡単にする。
社内サービスで使うつもり。
ざっくり、以上。
それこんなライブラリ使うともっと簡単にできるよ、とか、
車輪の再発明乙、とかだったら切ないので教えてもらえるとたすかりんこ。